
// Copyright (C) 2010-2011 Luca Piccioni
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//   
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace Derm.Raster
{
	/// <summary>
	/// 
	/// </summary>
	public class NetImagingImageCodecPlugin : ImageCodecPlugin
	{
		#region Derm Interoperability

		static void ConvertImageFormat(System.Drawing.Imaging.ImageFormat from, out Derm.Raster.ImageFormat to)
		{
			if (from.Guid == System.Drawing.Imaging.ImageFormat.Bmp.Guid) {
				to = ImageFormat.Bitmap;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Jpeg.Guid) {
				to = ImageFormat.Jpeg;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Png.Guid) {
				to = ImageFormat.Png;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Exif.Guid) {
				to = ImageFormat.Exif;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Tiff.Guid) {
				to = ImageFormat.Tiff;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Gif.Guid) {
				to = ImageFormat.Gif;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Icon.Guid) {
				to = ImageFormat.Ico;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Emf.Guid) {
				throw new ArgumentException(String.Format("not supported image format {0}", from));
				to = ImageFormat.Ico;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.MemoryBmp.Guid) {
				throw new ArgumentException(String.Format("not supported image format {0}", from));
				to = ImageFormat.Ico;
			} else if (from.Guid == System.Drawing.Imaging.ImageFormat.Wmf.Guid) {
				throw new ArgumentException(String.Format("not supported image format {0}", from));
				to = ImageFormat.Ico;
			} else
				throw new ArgumentException(String.Format("not supported image format {0}", from));
		}

		static void ConvertImageFormat(Derm.Raster.ImageFormat from, out System.Drawing.Imaging.ImageFormat to)
		{
			switch (from) {
				case ImageFormat.Bitmap:
					to = System.Drawing.Imaging.ImageFormat.Bmp;
					break;
				case ImageFormat.Jpeg:
					to = System.Drawing.Imaging.ImageFormat.Jpeg;
					break;
				case ImageFormat.Png:
					to = System.Drawing.Imaging.ImageFormat.Png;
					break;
				case ImageFormat.Exif:
					to = System.Drawing.Imaging.ImageFormat.Exif;
					break;
				case ImageFormat.Tiff:
					to = System.Drawing.Imaging.ImageFormat.Tiff;
					break;
				case ImageFormat.Gif:
					to = System.Drawing.Imaging.ImageFormat.Gif;
					break;
				case ImageFormat.Ico:
					to = System.Drawing.Imaging.ImageFormat.Icon;
					break;
				default:
					throw new ArgumentException("not supported image format", "from");
			}
		}

		static void ConvertPixelFormat(System.Drawing.Bitmap bitmap, out Pixel.Type to)
		{
			System.Drawing.Imaging.PixelFormat from = bitmap.PixelFormat;
			int flags = bitmap.Flags;

			bool sRGB = false;

			foreach (System.Drawing.Imaging.PropertyItem exifProperty in bitmap.PropertyItems) {
				switch (exifProperty.Id) {
					case ImageCodecPlugin.ExifTagColorSpace:
						ImageCodecPlugin.ExifColorSpace value = (ImageCodecPlugin.ExifColorSpace)BitConverter.ToUInt16(exifProperty.Value, 0);

						switch (value) {
							case ImageCodecPlugin.ExifColorSpace.sRGB:
								sRGB = true;
								break;
							default:
								break;
						}
						break;
					case ImageCodecPlugin.ExifTagGamma:
						UInt32 a1 = BitConverter.ToUInt32(exifProperty.Value, 0);
						UInt32 a2 = BitConverter.ToUInt32(exifProperty.Value, 4);
						Double gamma = (Double)a1 / (Double)a2;


						break;
				}
			}

			if ((flags & (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb) != 0) {
				ConvertPixelFormatRgb(from, out to, sRGB);
			} else if ((flags & (int)System.Drawing.Imaging.ImageFlags.ColorSpaceGray) != 0) {
				switch (from) {
					case System.Drawing.Imaging.PixelFormat.Format1bppIndexed:
					case System.Drawing.Imaging.PixelFormat.Format4bppIndexed:
					case System.Drawing.Imaging.PixelFormat.Format8bppIndexed:
						to = Pixel.Type.GRAY8;
						break;
					case System.Drawing.Imaging.PixelFormat.Format16bppGrayScale:
						to = Pixel.Type.GRAY16;
						break;
					case System.Drawing.Imaging.PixelFormat.Format32bppArgb:
						to = Pixel.Type.BGRA32;
						break;
					default:
						throw new ArgumentException(String.Format("GRAY pixel format {0} not supported", from));
				}
				
			} else if ((flags & (int)System.Drawing.Imaging.ImageFlags.ColorSpaceYcck) != 0) {
				throw new ArgumentException(String.Format("YCCK pixel format {0} not supported", from));
			} else if ((flags & (int)System.Drawing.Imaging.ImageFlags.ColorSpaceCmyk) != 0) {
				throw new ArgumentException(String.Format("CMYK pixel format {0} not supported", from));
			} else {
				ConvertPixelFormatRgb(from, out to, sRGB);
				//throw new ArgumentException(String.Format("not RGB/GRAY pixel format {0} not supported", from));
			}
		}

		private static void ConvertPixelFormatRgb(System.Drawing.Imaging.PixelFormat from, out Pixel.Type to, bool sRGB)
		{
			switch (from) {
				case System.Drawing.Imaging.PixelFormat.Format1bppIndexed:
				case System.Drawing.Imaging.PixelFormat.Format4bppIndexed:
				case System.Drawing.Imaging.PixelFormat.Format8bppIndexed:
					to = Pixel.Type.BGR24;
					break;
				case System.Drawing.Imaging.PixelFormat.Format16bppRgb555:
					to = Pixel.Type.BGR15;
					break;
				case System.Drawing.Imaging.PixelFormat.Format16bppRgb565:
					to = Pixel.Type.BGR16;
					break;
				case System.Drawing.Imaging.PixelFormat.Format24bppRgb:
					to = sRGB ? Pixel.Type.SBGR24 : Pixel.Type.BGR24;
					break;
				case System.Drawing.Imaging.PixelFormat.Format32bppRgb:
					to = Pixel.Type.BGRA32;
					break;
				case System.Drawing.Imaging.PixelFormat.Format16bppArgb1555:
					to = Pixel.Type.BGR15;
					break;
				case System.Drawing.Imaging.PixelFormat.Format16bppGrayScale:
					to = Pixel.Type.GRAY16;
					break;
				case System.Drawing.Imaging.PixelFormat.Format48bppRgb:
					to = Pixel.Type.BGR48;
					break;
				case System.Drawing.Imaging.PixelFormat.Format64bppPArgb:
					throw new ArgumentException(String.Format("RGB pixel format {0} not supported", from));
				case System.Drawing.Imaging.PixelFormat.Canonical:
					to = (sRGB == true) ? Pixel.Type.SBGR24 : Pixel.Type.BGR24;
					break;
				case System.Drawing.Imaging.PixelFormat.Format32bppArgb:
					to = Pixel.Type.BGRA32;
					break;
				case System.Drawing.Imaging.PixelFormat.Format64bppArgb:
					throw new ArgumentException(String.Format("RGB pixel format {0} not supported", from));
				case System.Drawing.Imaging.PixelFormat.Indexed:
				case System.Drawing.Imaging.PixelFormat.Alpha:
				case System.Drawing.Imaging.PixelFormat.PAlpha:
				case System.Drawing.Imaging.PixelFormat.Format32bppPArgb:
				default:
					throw new ArgumentException(String.Format("RGB pixel format {0} not supported", from));
			}
		}

		static void ConvertPixelFormat(Pixel.Type from, out System.Drawing.Imaging.PixelFormat to, out int flags)
		{
			switch (from) {
				case Pixel.Type.GRAY8:
					to = System.Drawing.Imaging.PixelFormat.Format16bppRgb555;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb;
					break;
				case Pixel.Type.GRAY16:
					to = System.Drawing.Imaging.PixelFormat.Format16bppGrayScale;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceGray;
					break;
				case Pixel.Type.BGR15:
					to = System.Drawing.Imaging.PixelFormat.Format16bppRgb555;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb;
					break;
				case Pixel.Type.BGR16:
					to = System.Drawing.Imaging.PixelFormat.Format16bppRgb565;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb;
					break;
				case Pixel.Type.BGR24:
					to = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb;
					break;
				case Pixel.Type.BGRA32:
					to = System.Drawing.Imaging.PixelFormat.Format32bppArgb;
					flags = (int)System.Drawing.Imaging.ImageFlags.ColorSpaceRgb;
					break;
				default:
					throw new ArgumentException(String.Format("pixel format {0} not supported", from));
			}
		}

		#endregion

		#region ImageCodecPlugin Overrides

		/// <summary>
		/// Plugin name.
		/// </summary>
		public override string Name { get { return ("NetImaging"); } }

		/// <summary>
		/// Determine whether this plugin is available for the current process.
		/// </summary>
		/// <returns>
		/// It returns a boolean value indicating whether the plugin is available for the current
		/// process.
		/// </returns>
		public override bool CheckAvailability()
		{
			return (true);
		}

		/// <summary>
		/// Get a list of image formats supported for reading.
		/// </summary>
		public override IEnumerable<ImageFormat> SupportedReadImageFormats
		{
			get {
				return (sSupportedFormats);
			}
		}

		/// <summary>
		/// Get a list of image formats supported for writing.
		/// </summary>
		public override IEnumerable<ImageFormat> SupportedWriteImageFormats
		{
			get {
				return (sSupportedFormats);
			}
		}

		/// <summary>
		/// Determine the plugin priority for a certain image format.
		/// </summary>
		/// <param name="format">
		/// An <see cref="ImageFormat"/> specifying the image format to test for priority.
		/// </param>
		/// <returns>
		/// It returns an integer value indicating the priority of this implementation respect other ones supporting the same
		/// image format. Conventionally, a value of 0 indicates a totally impartial plugin implementation; a value less than 0 indicates
		/// a more confident implementation respect other plugins; a value greater than 0 indicates a fallback implementation respect other
		/// plugins.
		/// 
		/// This implementation of this routine returns -1. The reasoning is that this plugin implementation is very slow in Query and Load, due
		/// the .NET abstraction. However, it is a very usefull fallback plugin since it can open the most of common image formats.
		/// </returns>
		public override int GetPriority(ImageFormat format)
		{
			switch (format) {
				default:
					return (-1);
			}
		}

		private static readonly ImageFormat[] sSupportedFormats = new ImageFormat[] {
			ImageFormat.Bitmap,
			ImageFormat.Jpeg,
			ImageFormat.Png,
			ImageFormat.Tiff,
			ImageFormat.Exif,
			ImageFormat.Gif,
		};
		
		/// <summary>
		/// Query image file informations.
		/// </summary>
		/// <returns>
		/// A <see cref="ImageInfo"/> specifying the image informations.
		/// </returns>
		public override ImageInfo QueryImageInfo(Stream fs, ImageCodec.CodecCriteria criteria)
		{
			ImageInfo info = new ImageInfo();
			
			using (System.Drawing.Bitmap iBitmap = new System.Drawing.Bitmap(fs)) {
				Pixel.Type iBitmapPixelType;

				ConvertImageFormat(iBitmap.RawFormat, out info.ContainerFormat);
				ConvertPixelFormat(iBitmap, out iBitmapPixelType);
				info.PixelType = iBitmapPixelType;
				info.Width = (uint)iBitmap.Width;
				info.Height = (uint)iBitmap.Height;
			}

			return (info);
		}
		
		/// <summary>
		/// Load image pixels from file.
		/// </summary>
		/// <returns>
		/// A <see cref="Image"/>
		/// </returns>
		/// <param name="criteria">
		/// A <see cref="ImageCodec.CodecCriteria"/> specifying parameters for loading an image stream.
		/// </param>
		public override Image Load(Stream fs, ImageCodec.CodecCriteria criteria)
		{
			using (System.Drawing.Bitmap iBitmap = new System.Drawing.Bitmap(fs)) {
				Image image = null;
				Pixel.Type pType, pConvType = Pixel.Type.None;

				// Allocate image raster
				ConvertPixelFormat(iBitmap, out pType);

				// Check for hardware/software support
				if (Pixel.IsSupportedInternalFormat(pType) == false) {
					if ((criteria.IsDefined(ImageCodec.HardwareSupport) == false) || ((Boolean)criteria[ImageCodec.HardwareSupport] == false)) {
						// Pixel type not directly supported by hardware... try to guess suitable software conversion
						pConvType = Pixel.GuessBestSupportedConvertion(pType);
						if (pConvType == Pixel.Type.None)
							throw new InvalidOperationException("pixel type " + pType.ToString() + " is not supported by hardware neither software");
					} else
						throw new InvalidOperationException("pixel type " + pType.ToString() + " is not supported by hardware");
				} else
					pConvType = pType;

				image = Image.CreateImage((uint)iBitmap.Width, (uint)iBitmap.Height, pType);

				switch (iBitmap.PixelFormat) {
					case System.Drawing.Imaging.PixelFormat.Format1bppIndexed:
					case System.Drawing.Imaging.PixelFormat.Format4bppIndexed:
					case System.Drawing.Imaging.PixelFormat.Format8bppIndexed:
						LoadBitmapByClone(iBitmap, image);
						break;
					default:
						LoadBitmapByLockBits(iBitmap, image);
						break;
				}

				// Convert image to supported format, if necessary
				if ((pConvType != Pixel.Type.None) && (pConvType != pType))
					image = image.Convert(pConvType);
				
				return (image);
			}
		}

		/// <summary>
		/// Special method for creating Image from Bitmap.
		/// </summary>
		/// <param name="iBitmap"></param>
		/// <param name="criteria"></param>
		/// <returns></returns>
		internal static Image LoadFromBitmap(System.Drawing.Bitmap iBitmap, ImageCodec.CodecCriteria criteria)
		{
			Image image = null;
			Pixel.Type pType, pConvType = Pixel.Type.None;

			// Allocate image raster
			ConvertPixelFormat(iBitmap, out pType);

			// Check for hardware/software support
			if (Pixel.IsSupportedInternalFormat(pType) == false) {
				if ((criteria.IsDefined(ImageCodec.HardwareSupport) == false) || ((Boolean)criteria[ImageCodec.HardwareSupport] == false)) {
					// Pixel type not directly supported by hardware... try to guess suitable software conversion
					pConvType = Pixel.GuessBestSupportedConvertion(pType);
					if (pConvType == Pixel.Type.None)
						throw new InvalidOperationException("pixel type " + pType.ToString() + " is not supported by hardware neither software");
				} else
					throw new InvalidOperationException("pixel type " + pType.ToString() + " is not supported by hardware");
			} else
				pConvType = pType;

			image = Image.CreateImage((uint)iBitmap.Width, (uint)iBitmap.Height, pType);

			switch (iBitmap.PixelFormat) {
				case System.Drawing.Imaging.PixelFormat.Format1bppIndexed:
				case System.Drawing.Imaging.PixelFormat.Format4bppIndexed:
				case System.Drawing.Imaging.PixelFormat.Format8bppIndexed:
					LoadBitmapByClone(iBitmap, image);
					break;
				default:
					LoadBitmapByLockBits(iBitmap, image);
					break;
			}

			// Convert image to supported format, if necessary
			if ((pConvType != Pixel.Type.None) && (pConvType != pType))
				image = image.Convert(pConvType);
			
			return (image);
		}

		private static void LoadBitmapByLockBits(System.Drawing.Bitmap iBitmap, Image image)
		{
			System.Drawing.Imaging.BitmapData iBitmapData = null;
			GCHandle hImageData = GCHandle.Alloc(image.GetPixelData(), GCHandleType.Pinned);

			try {
				// Obtain source and destination data pointers
				iBitmapData = iBitmap.LockBits(new System.Drawing.Rectangle(0, 0, iBitmap.Width, iBitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, iBitmap.PixelFormat);

				// Copy Bitmap data dst Image
				unsafe {
					byte* hImageDataPtr = (byte*)hImageData.AddrOfPinnedObject().ToPointer();
					byte* iBitmapDataPtr = (byte*)iBitmapData.Scan0.ToPointer();
					uint hImageDataStride = image.Stride;
					uint iBitmapDataStride = (uint)iBitmapData.Stride;
					
					if (hImageDataStride > iBitmapDataStride)
						throw new InvalidOperationException("invalid bitmap stride");

					// Note:
					// .NET Image library stores bitmap scan line data in memory padded dst 4 bytes boundaries
					// .NET Image Library present a bottom up image, so invert the scan line order

					iBitmapDataPtr = iBitmapDataPtr + ((image.Height-1) * iBitmapDataStride);

					for (uint line = 0; line < image.Height; line++, hImageDataPtr += hImageDataStride, iBitmapDataPtr -= iBitmapDataStride)
						memcpy((void*)hImageDataPtr, (void*)iBitmapDataPtr, (int)hImageDataStride);
				}
			} finally {
				if (hImageData.IsAllocated == true)
					hImageData.Free();
				if (iBitmapData != null)
					iBitmap.UnlockBits(iBitmapData);
			}
		}

		private static void LoadBitmapByClone(System.Drawing.Bitmap iBitmap, Image image)
		{
			System.Drawing.Imaging.PixelFormat iBitmapFormat;
			int iBitmapFlags;

			// Determine the clone bitmap pixel format
			ConvertPixelFormat(image.PixelFormat, out iBitmapFormat, out iBitmapFlags);
			// Clone image converting the pixel format
			using (System.Drawing.Bitmap iClonedBitmap = iBitmap.Clone(new System.Drawing.Rectangle(0, 0, iBitmap.Width, iBitmap.Height), iBitmapFormat)) {
				LoadBitmapByLockBits(iClonedBitmap, image);
			}
		}

		/// <summary>
		/// Save image pixels dst file.
		/// </summary>
		/// <param name="fs">
		/// A <see cref="System.IO.FileStream"/> which stores the image data.
		/// </param>
		/// <param name="image">
		/// A <see cref="Image"/> holding the data dst be stored.
		/// </param>
		/// <param name="criteria">
		/// A <see cref="ImageCodec.CodecCriteria"/> specifying parameters for loading an image stream.
		/// </param>
		public override void Save(Stream fs, Image image, ImageFormat format, ImageCodec.CodecCriteria criteria)
		{
			System.Drawing.Bitmap iBitmap;
			System.Drawing.Imaging.ImageFormat iBitmapFormat;
			System.Drawing.Imaging.PixelFormat iBitmapPixelFormat;
			int iBitmapFlags;

			ConvertImageFormat(format, out iBitmapFormat);
			ConvertPixelFormat(image.PixelFormat, out iBitmapPixelFormat, out iBitmapFlags);

			// Obtain source and destination data pointers
			using (iBitmap = new System.Drawing.Bitmap((int)image.Width, (int)image.Height, iBitmapPixelFormat)) {
				System.Drawing.Imaging.BitmapData iBitmapData = null;
				GCHandle hImageData = GCHandle.Alloc(image.GetPixelData(), GCHandleType.Pinned); 

				try {
					iBitmapData = iBitmap.LockBits(new System.Drawing.Rectangle(0, 0, iBitmap.Width, iBitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, iBitmap.PixelFormat);

					// Copy Image data dst Bitmap
					unsafe {
						byte* hImageDataPtr = (byte*)hImageData.AddrOfPinnedObject().ToPointer();
						byte* iBitmapDataPtr = (byte*)iBitmapData.Scan0.ToPointer();
						uint hImageDataStride = image.Stride;
						uint iBitmapDataStride = (uint)iBitmapData.Stride;
						
						// Note:
						// .NET Image Library stores bitmap scan line data in memory padded dst 4 bytes boundaries
						// .NET Image Library expect a bottom up image, so invert the scan line order

						iBitmapDataPtr = iBitmapDataPtr + ((image.Height-1) * iBitmapDataStride);

						for (uint line = 0; line < image.Height; line++, hImageDataPtr += hImageDataStride, iBitmapDataPtr -= iBitmapDataStride)
							memcpy((void*)iBitmapDataPtr, (void*)hImageDataPtr, (int)hImageDataStride);
					}
					hImageData.Free();
					iBitmap.UnlockBits(iBitmapData);
					iBitmapData = null;

					// Save image with the specified format
					iBitmap.Save(fs, iBitmapFormat);
				} finally {
					if (hImageData.IsAllocated == true)
						hImageData.Free();
					if (iBitmapData != null)
						iBitmap.UnlockBits(iBitmapData);
				}
			}
		}

		[System.Security.SuppressUnmanagedCodeSecurity()]
		[System.Runtime.InteropServices.DllImport(Library, EntryPoint = "memcpy", ExactSpelling = true)]
		internal extern static unsafe void memcpy(void *dst, void *src, int bytes);

#if WIN32
		/// <summary>
		/// Library exporting C memcpy.
		/// </summary>
		private const string Library = "msvcrt.dll";
#endif

#if __linux__
		/// <summary>
		/// Library exporting C memcpy.
		/// </summary>
		private const string Library = "libstdc.so";
#endif

		#endregion
	}
}
